package org.mx.service.rest.graphql;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import graphql.ExceptionWhileDataFetching;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.GraphQLError;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import org.mx.StringUtils;
import org.mx.error.UserInterfaceException;
import org.mx.error.UserInterfaceSystemErrorException;
import org.mx.service.error.UserInterfaceServiceErrorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/**
 * 描述： 使用GraphQL进行快速RESTful重构的工具类
 *
 * @author john peng Date time 2018/10/4 下午1:55
 */
public class GraphQLUtils {
	private static final Logger logger = LoggerFactory.getLogger(GraphQLUtils.class);

	private GraphQL graphQL = null;

	/**
	 * 从一个Map中解析为指定的对象
	 * 
	 * @param <T>   泛型定义
	 * @param input 输入Map数据
	 * @param clazz 指定的类
	 * @return 指定类的对象
	 */
	public static <T> T parse(Map<String, Object> input, Class<T> clazz) {
		if (input == null || input.isEmpty()) {
			return null;
		}
		return JSON.parseObject(new JSONObject(input).toJSONString(), clazz);
	}

	private InputStream[] getInputStreamsWithClassPath(String names) {
		String[] classpathNames = StringUtils.split(names, ",", true, true);
		InputStream[] inputStreams = new InputStream[classpathNames.length];
		for (int index = 0; index < classpathNames.length; index++) {
			inputStreams[index] = GraphQLUtils.class.getResourceAsStream(classpathNames[index]);
		}
		return inputStreams;
	}

	/**
	 * 根据指定的GraphQL模式文件初始化GraphQL
	 *
	 * @param names         GraphQL模式文件名列表，逗号分割
	 * @param executionList 类型执行器列表
	 */
	public void initByClasspath(String names, List<GraphQLTypeExecution> executionList) {
		InputStream[] inputStreams = getInputStreamsWithClassPath(names);
		init(inputStreams, executionList);
		if (logger.isDebugEnabled()) {
			logger.debug(String.format("Initialize the graphql schema with classpath: schemas: %s.", names));
		}
	}

	@SuppressWarnings("unchecked")
	private void initByFileCommon(String names, Object context) {
		String[] filenames = StringUtils.split(names, ",", true, true);
		FileInputStream[] fileInputStreams = new FileInputStream[filenames.length];
		try {
			for (int index = 0; index < filenames.length; index++) {
				fileInputStreams[index] = new FileInputStream(filenames[index]);
			}
			if (logger.isDebugEnabled()) {
				logger.debug(String.format("Initialize the graphql schema with file: schemas: %s.", names));
			}
			if (context instanceof ApplicationContext) {
				init(fileInputStreams, (ApplicationContext) context);
			} else {
				init(fileInputStreams, (List<GraphQLTypeExecution>) context);
			}
		} catch (IOException ex) {
			if (logger.isErrorEnabled()) {
				logger.error(String.format("The file[%s] I/O fail.", names), ex);
			}
			throw new UserInterfaceServiceErrorException(
					UserInterfaceServiceErrorException.ServiceErrors.GRAPHQL_FILE_READ_FAIL);
		} finally {
			for (FileInputStream fin : fileInputStreams) {
				if (fin != null) {
					try {
						fin.close();
					} catch (IOException ex) {
						if (logger.isErrorEnabled()) {
							logger.error(String.format("Close the schema file fail, names: %s.", names), ex);
						}
					}
				}
			}
		}
	}

	/**
	 * 根据指定的GraphQL模式文件（包路径方式）初始化GraphQL
	 *
	 * @param names         GraphQL模式文件名列表，逗号分割
	 * @param executionList 类型执行器列表
	 */
	public void initByFile(String names, List<GraphQLTypeExecution> executionList) {
		initByFileCommon(names, executionList);
	}

	/**
	 * 根据指定的GraphQL模式文件输入流初始化GraphQL
	 *
	 * @param in             GraphQL模式文件输入流数组
	 * @param executionsList 类型执行器列表
	 */
	public void init(InputStream[] in, List<GraphQLTypeExecution> executionsList) {
		// 加载SDL的Schema文件
		if (executionsList == null || executionsList.isEmpty()) {
			if (logger.isErrorEnabled()) {
				logger.error("The execution list is empty.");
			}
			throw new UserInterfaceSystemErrorException(
					UserInterfaceSystemErrorException.SystemErrors.SYSTEM_ILLEGAL_PARAM);
		}
		Consumer<RuntimeWiring.Builder> consumer = builder -> {
			for (GraphQLTypeExecution typeExecution : executionsList) {
				if (typeExecution == null || StringUtils.isBlank(typeExecution.getTypeName())) {
					if (logger.isErrorEnabled()) {
						logger.error("The GraphQL executions is null or it's type name is blank.");
					}
					throw new UserInterfaceSystemErrorException(
							UserInterfaceSystemErrorException.SystemErrors.SYSTEM_ILLEGAL_PARAM);
				}
				String typeName = typeExecution.getTypeName();
				List<GraphQLField> fieldExecutions = typeExecution.getExecutions();
				if (fieldExecutions == null || fieldExecutions.isEmpty()) {
					if (logger.isErrorEnabled()) {
						logger.error(String.format("The type[%s]'s execution is null or empty.", typeName));
					}
					throw new UserInterfaceSystemErrorException(
							UserInterfaceSystemErrorException.SystemErrors.SYSTEM_ILLEGAL_PARAM);
				}
				builder.type(typeName, typeWiring -> {
					fieldExecutions.forEach(fieldExecution -> {
						if (fieldExecution == null || StringUtils.isBlank(fieldExecution.getFieldName())) {
							if (logger.isWarnEnabled()) {
								logger.warn(String.format(
										"The type[%s]'s field execution is null or the field name is blank.",
										typeName));
							}
						} else {
							if (fieldExecution instanceof GraphQLFieldSingleResult) {
								// 根据字段名创建一个获取单条记录的执行器
								typeWiring.dataFetcher(fieldExecution.getFieldName(),
										((GraphQLFieldSingleResult<?>) fieldExecution)::executeForSingle);
							}
							if (fieldExecution instanceof GraphQLFieldListResult) {
								// 根据字段名+List创建一个获取多条记录的执行器
								typeWiring.dataFetcher(String.format("%sList", fieldExecution.getFieldName()),
										((GraphQLFieldListResult<?>) fieldExecution)::executeForList);
							}
						}
					});
					return typeWiring;
				});
			}
		};
		init(in, consumer);
	}

	/**
	 * 根据指定的GraphQL模式文件初始化GraphQL
	 *
	 * @param names   GraphQL模式文件名
	 * @param context Spring IoC容器上下文
	 */
	public void initByClasspath(String names, ApplicationContext context) {
		InputStream[] inputStreams = getInputStreamsWithClassPath(names);
		init(inputStreams, context);
		if (logger.isDebugEnabled()) {
			logger.debug(String.format("Initialize the graphql schema with classpath: schemas: %s.", names));
		}
	}

	/**
	 * 根据指定的GraphQL模式文件（包路径方式）初始化GraphQL
	 *
	 * @param names   GraphQL模式文件名列表，逗号分割
	 * @param context Spring IoC容器上下文
	 */
	public void initByFile(String names, ApplicationContext context) {
		initByFileCommon(names, context);
	}

	/**
	 * 根据指定的GraphQL模式文件输入流初始化GraphQL
	 *
	 * @param inputStreams GraphQL模式文件输入流数组
	 * @param context      Spring IoC容器上下文
	 */
	public void init(InputStream[] inputStreams, ApplicationContext context) {
		if (context == null) {
			if (logger.isErrorEnabled()) {
				logger.error("The application context is null.");
			}
			throw new UserInterfaceSystemErrorException(
					UserInterfaceSystemErrorException.SystemErrors.SYSTEM_ILLEGAL_PARAM);
		}
		Map<String, GraphQLAnnotationExecution> executionMap = context.getBeansOfType(GraphQLAnnotationExecution.class);
		if (executionMap.isEmpty()) {
			if (logger.isWarnEnabled()) {
				logger.warn("There may not has any graphql annotation type.");
			}
			return;
		}
		Consumer<RuntimeWiring.Builder> consumer = builder -> {
			for (GraphQLAnnotationExecution execution : executionMap.values()) {
				Method[] methods = execution.getClass().getMethods();
				for (Method method : methods) {
					GraphQLFieldAnnotation annotation = method.getAnnotation(GraphQLFieldAnnotation.class);
					if (annotation != null) {
						String name = annotation.value();
						if (StringUtils.isBlank(name)) {
							if (logger.isErrorEnabled()) {
								logger.error("The annotation's field name is blank.");
							}
							throw new UserInterfaceSystemErrorException(
									UserInterfaceSystemErrorException.SystemErrors.SYSTEM_ILLEGAL_PARAM);
						}
						builder.type(execution.getTypeName(), typeWiring -> {
							typeWiring.dataFetcher(name, environment -> {
								try {
									return method.invoke(execution, environment);
								} catch (IllegalAccessException | InvocationTargetException ex) {
									if (ex.getCause() instanceof UserInterfaceException) {
										throw (UserInterfaceException) ex.getCause();
									} else {
										if (logger.isErrorEnabled()) {
											logger.error(String.format("Invoke method[%s] fail.", method.getName()),
													ex);
										}
										throw new UserInterfaceServiceErrorException(
												UserInterfaceServiceErrorException.ServiceErrors.GRAPHQL_EXECUTE_FAIL);
									}
								}
							});
							return typeWiring;
						});
					}
				}
			}
		};
		init(inputStreams, consumer);
	}

	/**
	 * 初始化GraphQL
	 *
	 * @param inputStreams GrahpQL模式文件输入流数组
	 * @param consumer     自定义的消费函数
	 */
	private void init(InputStream[] inputStreams, Consumer<RuntimeWiring.Builder> consumer) {
		if (inputStreams == null || inputStreams.length <= 0) {
			if (logger.isErrorEnabled()) {
				logger.error("There hasn't any graphql schema input stream.");
			}
			throw new UserInterfaceSystemErrorException(
					UserInterfaceSystemErrorException.SystemErrors.SYSTEM_ILLEGAL_PARAM);
		}
		SchemaParser parser = new SchemaParser();
		TypeDefinitionRegistry registry = null;
		for (InputStream in : inputStreams) {
			if (registry == null) {
				registry = parser.parse(new InputStreamReader(in));
			} else {
				registry.merge(parser.parse(new InputStreamReader(in)));
			}
		}

		// 根据自定义实现装配RuntimeWiring
		RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring();
		consumer.accept(builder);
		RuntimeWiring wiring = builder.build();
		// 初始化GraphQL
		SchemaGenerator generator = new SchemaGenerator();
		GraphQLSchema schema = generator.makeExecutableSchema(registry, wiring);
		graphQL = GraphQL.newGraphQL(schema).build();
	}

	/**
	 * 执行一个GraphQL请求
	 *
	 * @param request GraphQL请求字符串
	 * @return 执行结果，采用JSONObject结构
	 */
	public JSONObject execute(String request) {
		ExecutionResult result = graphQL.execute(request);
		List<GraphQLError> errors = result.getErrors();
		if (errors != null && !errors.isEmpty()) {
			// error
			if (errors.get(0) instanceof ExceptionWhileDataFetching
					&& ((ExceptionWhileDataFetching) errors.get(0)).getException() instanceof UserInterfaceException) {
				throw (UserInterfaceException) ((ExceptionWhileDataFetching) errors.get(0)).getException();
			}
			StringBuffer sb = new StringBuffer("errors: ");
			if (logger.isErrorEnabled()) {
				errors.forEach(error -> sb
						.append(String.format("[(%s): %s.]", error.getErrorType().name(), error.getMessage())));
				logger.error(sb.toString());
			}
			throw new RuntimeException(sb.toString());
		} else {
			// success
			return new JSONObject(result.getData());
		}
	}
}
